/*
 * File:    Authentication.java
 * Created: 17-Jan-2008
 * Version: $Id$
 *
 * COPYRIGHT (C) 2008, Bitgate Software, LLC.  All Rights Reserved.
 * Released under the Creative Commons License version 2.5
 *
 * software@bitgatesoftware.com
 */

package com.bitgate.util.service.protocol.http.db;

import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;

import com.bitgate.util.bytes.ByteString;
import com.bitgate.util.encoder.Base64;
import com.bitgate.util.service.protocol.http.HttpClientContext;
import com.bitgate.util.service.protocol.http.handler.HandlerException;
import com.bitgate.util.service.protocol.http.handler.HttpHeaders;
import com.bitgate.util.sql.Registry;

import static com.bitgate.util.debug.Debug.debug;
import static com.bitgate.util.debug.Debug.isDebugEnabled;

/**
 * THis class handles the authentication mechanism for HTTP(S) requests made to a server.
 * 
 * @author kenji
 * @since BSSE 0.6.0
 */
public class Authentication
{
	private HashMap<String, Integer> docRoots;
	private static final Authentication _default = new Authentication();
	
	private static final String HTPASSWD_LOOKUP = "SELECT id FROM htpasswd WHERE username = ? AND password = ?";
	private static final String DOCROOT_LOOKUP  = "SELECT id FROM docroot WHERE docroot = ?";
	private static final String HTPASSWD_DOCROOT_MATCH_LOOKUP = "SELECT COUNT(*) AS cnt FROM htpasswd_docroot WHERE " +
		"htpasswd_id = ? AND docroot_id = ?";
	
	/**
	 * Default constructor.
	 */
	public Authentication() {
		docRoots = new HashMap<String, Integer>();
	}
	
	/**
	 * Returns the default instance of this class.
	 * 
	 * @return <code>static Authentication</code> object.
	 */
	public static Authentication getDefault() {
		return _default;
	}
	
	/**
	 * Determines whether or not a user authenticates to the given username and password.
	 * 
	 * @param dbName The name of the database to check.
	 * @param username The username to check.
	 * @param password The password to check.
	 * @return <code>true</code> if the user was authenticated, <code>false</code> otherwise.
	 */
	public int findUser(String dbName, String username, String password) {
		Connection dbConnection = Registry.getDefault().get(dbName);
		PreparedStatement pStatement = null;
		
		try {
			pStatement = dbConnection.prepareStatement(HTPASSWD_LOOKUP);
			pStatement.setString(1, username);
			pStatement.setString(2, password);
		} catch(SQLException e) {
			if (isDebugEnabled()) {
				debug("Exception on query for '" + HTPASSWD_LOOKUP + "': " + e.getMessage());
			}
			
			return 0;
		}
		
		ResultSet rSet = null;
		int userId = 0;
		
		try {
			rSet = pStatement.executeQuery();
			rSet.next();
			userId = rSet.getInt(1);
			rSet.close();
		} catch(SQLException e) {
			if (isDebugEnabled()) {
				debug("Exception on query for '" + HTPASSWD_LOOKUP + "': " + e.getMessage());
			}
			
			return 0;
		} finally {
			try {
				pStatement.close();
			} catch(SQLException e) {
				// Do nothing.
			}
			
			try {
				dbConnection.close();
			} catch(SQLException e) {
				// Do nothing.
			}
		}
		
		return userId;
	}
	
	/**
	 * Finds a docroot entry by ID for quicker lookup.  Checks the path given, and returns the match.
	 * 
	 * @param dbName The name of the database to check.
	 * @param docroot The root document directory to check.
	 * @return <code>int</code> containing the found ID, <code>0</code> if not located.
	 */
	public int findDocroot(String dbName, String docroot) {
		if (isDebugEnabled()) {
			debug("Finding docroot for dbname '" + dbName + "' docroot '" + docroot + "'");
		}
		
		String docroots[] = docroot.split("\\/");
		String docrootComparison = "";
		int docrootId = 0;
		
		for(int i = 0; i < docroots.length; i++) {
			Connection dbConnection = Registry.getDefault().get(dbName);
			PreparedStatement pStatement = null;
			
			docrootComparison += "/" + docroots[i]; 

			// Bypass lookup if in cache.
			
			if (isDebugEnabled()) {
				debug("Checking docroot entry '" + docrootComparison + "'");
			}
			
			if (docRoots.get(docrootComparison) != null) {
				docrootId = docRoots.get(docrootComparison).intValue();
				
				if (isDebugEnabled()) {
					debug("Pulled docroot '" + docrootComparison + "' from cache id '" + docrootId + "'");
				}
				
				continue;
			}
			
			// Else, look up the entry, and store it if found.
			
			try {
				pStatement = dbConnection.prepareStatement(DOCROOT_LOOKUP);
				pStatement.setString(1, docrootComparison);
			} catch(SQLException e) {
				if (isDebugEnabled()) {
					debug("Exception on query for '" + DOCROOT_LOOKUP + "': " + e.getMessage());
				}
				
				return 0;
			}
			
			ResultSet rSet = null;
			
			try {
				rSet = pStatement.executeQuery();
				rSet.next();
				int tempId = rSet.getInt(1);
				rSet.close();
				
				if (tempId > 0) {
					docrootId = tempId;
					docRoots.put(docrootComparison, Integer.valueOf(docrootId));
					
					if (isDebugEnabled()) {
						debug("Cache update: Docroot '" + docrootComparison + "' id '" + docrootId + "'");
					}
				}
			} catch(SQLException e) {
				debug("SQL Exception: " + e.getMessage());
			}
			
			try {
				pStatement.close();
			} catch(SQLException e) {
				// No error occurred, really.
			}
			
			try {
				dbConnection.close();
			} catch(SQLException e) {
				// Do nothing.
			}
		}
		
		return docrootId;
	}
	
	/**
	 * Determines whether or not the docroot specified requires authentication.
	 * 
	 * @param dbName The database name to check.
	 * @param docroot The name of the docroot to check.
	 * @return <code>true</code> if authentication is required, <code>false</code> otherwise.
	 */
	public boolean needsAuthentication(String dbName, String docroot) {
		return (findDocroot(dbName, docroot) > 0);
	}
	
	/**
	 * Indicates whether or not the user provided - by ID - is authorized to access a specified docroot.
	 * 
	 * @param dbName The database name to check.
	 * @param userId The ID of the authenticated user.
	 * @param docroot The docroot to check against.
	 * @return <code>true</code> if authenticated, <code>false</code> otherwise.
	 */
	public boolean isAuthenticated(String dbName, int userId, String docroot) {
		int docrootId = findDocroot(dbName, docroot);
		
		if (docrootId > 0) {
			Connection dbConnection = Registry.getDefault().get(dbName);
			PreparedStatement pStatement = null;
			
			try {
				pStatement = dbConnection.prepareStatement(HTPASSWD_DOCROOT_MATCH_LOOKUP);
				pStatement.setInt(1, userId);
				pStatement.setInt(2, docrootId);
			} catch(SQLException e) {
				if (isDebugEnabled()) {
					debug("Exception on query for '" + HTPASSWD_DOCROOT_MATCH_LOOKUP + "': " + e.getMessage());
				}
				
				return false;
			}
			
			ResultSet rSet = null;
			
			try {
				rSet = pStatement.executeQuery();
				rSet.next();
				
				if (rSet.getInt(1) > 0) {
					return true;
				}
				
				rSet.close();
			} catch(SQLException e) {
				return false;
			} finally {
				try {
					pStatement.close();
				} catch(SQLException e) {
					// Do nothing.
				}
				
				try {
					dbConnection.close();
				} catch(SQLException e) {
					// Do nothing.
				}
			}
		}
		
		return false;
	}
	
	/**
	 * Performs HTTP verification from a specified initial request on a root directory, with a given authorization
	 * field.
	 * 
	 * @param dbName The database name to check.
	 * @param hcContext The <code>HttpClientContext</code> object containing the currently active user.
	 * @param rootDirectory The docroot directory to check.
	 * @param initialRequest The initial request made by the user.
	 * @return <code>true</code> if authenticated, <code>false</code> otherwise.
	 * @throws HandlerException on any errors.
	 */
	public boolean authenticate(String dbName, HttpClientContext hcContext, String rootDirectory, String initialRequest)
		throws HandlerException {
		ByteString authentication = hcContext.getHeader(new ByteString("authorization"));
		String authenticationDecoded = null;

		if (authentication != null) {
			authentication = authentication.substring(6);
			authenticationDecoded = new String(Base64.decode(authentication.toString()));
		}
		
		if (isDebugEnabled()) {
			if (authentication != null) {
				debug("Authentication sent='" + authentication.toString() + "', decoded='" + authenticationDecoded + "'");
			}
		}
		
		if (Authentication.getDefault().needsAuthentication(dbName, rootDirectory + initialRequest)) {
			String authUser = null, authPass = null;
			boolean isAuthenticated = false;
			
			if (authenticationDecoded != null) {
				int authenticationColon;
				
				if ((authenticationColon = authenticationDecoded.indexOf(':')) != -1) {
					authUser = authenticationDecoded.substring(0, authenticationColon);
					authPass = authenticationDecoded.substring(authenticationColon + 1);
				}
			}
			
			if (authUser != null) {
				int authUserId = Authentication.getDefault().findUser(dbName, authUser, authPass);
				
				if (isDebugEnabled()) {
					debug("AuthUser=" + authUser + " AuthPass=" + authPass + " UserID=" + authUserId);
				}
				
				if (authUserId != 0) {
					isAuthenticated = (Authentication.getDefault().isAuthenticated(dbName, authUserId,
							rootDirectory + initialRequest));
				}
			}
			
			if (!isAuthenticated) {
				if (isDebugEnabled()) {
					debug("Docroot '" + rootDirectory + "' requires user/pass authentication.");
				}
				
				hcContext.setMimeType("text/html");
				
				try {
					StringBuffer pageOut = new StringBuffer();
					
					pageOut.append("Requested page '");
					pageOut.append(initialRequest);
					pageOut.append("' requires authentication.");
					
					hcContext.setRequestResult(HttpHeaders.HeaderResult.HTTP_UNAUTHORIZED);
					hcContext.write(HttpHeaders.createHeader(HttpHeaders.HeaderResult.HTTP_UNAUTHORIZED, hcContext,
						pageOut.length(), null, null, new String[] { "WWW-Authenticate: Basic realm=\"Login\"" }).toString().getBytes());
					hcContext.write(pageOut.toString().getBytes());
				} catch(IOException e) {
					throw new HandlerException("Unable to write data", e);
				}
				
				return false;
			}
			
			if (isDebugEnabled()) {
				debug("User/Pass credentials '" + authenticationDecoded + "' passes authentication.");
			}
		}
		
		return true;
	}
}